Skip to content

feat: 사전 투표창 + 홈 .pen 정밀 매칭 + Poll API · Skeleton 인프라#34

Merged
Roy-wonji merged 21 commits into
developfrom
feature/-PreVote
May 19, 2026
Merged

feat: 사전 투표창 + 홈 .pen 정밀 매칭 + Poll API · Skeleton 인프라#34
Roy-wonji merged 21 commits into
developfrom
feature/-PreVote

Conversation

@Roy-wonji
Copy link
Copy Markdown
Contributor

요약

Refs (부분 완료 / 후속 PR 필요)

  • Refs #4 — 사전 투표창 UI + Poll detail GET 만 완료, 사후 투표·재투표 / vote 제출(post pre/post) / vote-stats 미구현
  • Refs #3 — 홈 메인 + 카드 진입 라우팅 완료, 큐레이팅/알림 모달 미구현
  • Refs #18 — 사전 투표창 + 홈 카드 KFImage placeholder 만 적용, 다른 화면 후속
  • Refs #19 — 보호 이미지 인증 첨부 완료
  • Refs #24 — Tokens Studio 코드젠 정렬 적용
  • Refs #31 — Apple identityToken 본 PR 에 포함

테스트 플랜

  • 홈 진입 시 fetchHome 정상 → editorPicks 이미지(보호 리소스) Bearer 토큰으로 로딩
  • 홈 → Vote 카드 탭 → 사전 투표창 진입 → onAppear 시 fetchPoll → 응답 도착 전 PreVoteSkeletonView (shimmer) → 응답 후 loadedContent
  • Vote 카드 옵션 탭 시 빈칸이 옵션 텍스트로 채워지고 percentage bar 가 0 → target 으로 채워짐 (0.6s easeOut)
  • 사전 투표창 공유 버튼 → 시스템 share sheet (UIActivityViewController) 표시
  • Apple 로그인 진입 시 Authorization body 에 identityToken 포함 (Google·Kakao 는 null)
  • LoginView 색상 (.neutral200, .borderGrayDefault) 정상 렌더링

Roy-wonji added 17 commits May 17, 2026 22:18
- PreVoteBattle / PreVoteOption / PhilosopherAvatar Entity + .mock
- PreVote 폴더 (Reducer · View) — 2지선다 옵션 카드 + VS 뱃지 + Pretendard 24pt 타이틀 + 그라데이션 콘텐츠 영역, 탭바 hidden
- HomeCoordinator 라우터에 .preVote 푸시 / dismiss·submit → backAction 위임
- DesignSystem: 철학자 아바타 자산 (플라톤·사르트르·순자) + ImageAsset case 추가
- DesignSystem 공통 컴포넌트: PickeNavigationBar (좌측 back · 가운데 옵션 · 우측 ViewBuilder), 시스템 공유용 ShareSheet (UIActivityViewController wrapper), CustomButton height override (CTAButtonSize 기본값 + 명시적 height)
- AGENTS.md: Coordinator 의 `extension X { @Reducer public enum XScreen { ... } }` 구조 절대 건드리지 말 것 규칙 명시
- HomeFeature.View 에 heroTapped · hotBattleTapped · bestBattleTapped · newBattleTapped 추가
- DelegateAction.presentPreVote(battleId:) 시그니처로 통일하여 카드 종류 무관하게 battleId 만 코디네이터에 전달
- HomeView 의 카드들에 .contentShape(Rectangle()) + .onTapGesture 부착, Vote 카드는 Button 래핑 제거
- HeroCarouselView 에 onTap 클로저 매개변수 추가
- 홈 배경을 .pen `#fafaf9` 매칭을 위해 .beige50 → .beige200 으로 보정
- NewBattleCardView 재작성 — Admission 옵션 카드 (아바타 40×40 + 입장 + 철학자) · VS 뱃지 24×24 secondary200 · beige300/beige600 stroke
- 아바타: PhilosopherAvatar 매핑된 일러스트 fallback · 추후 API 의 philosopherAImageUrl/BImageUrl 들어오면 KFImage 로 우선 표시
- NewBattle Entity 에 philosopherAImageURL · philosopherBImageURL 복원 (mock 은 picsum URL)
- HomeHeaderView 하단에 1pt beige600 stroke overlay 추가 — .pen Header bottom stroke 매칭
- GNB 아이콘 PNG → SVG 로 교체 (preserves-vector-representation · template-rendering-intent: original)
- inactive 4개 + active 4개 총 8 imageset 등록 (Icon-홈/탐색/빠른배틀/마이 × default·active)
- MainTabCoordinator.Tab.iconAsset(isSelected:) 함수화하여 선택 상태에 따라 active 자산 반환
- MainTabView.tabIcon 에서 selectedTab 비교로 isSelected 분기 + .renderingMode(.original) 강제하여 SVG 원본 색 유지 (시스템 tint 적용 X)
- HomeService 등이 Entity 타입을 직접 참조할 수 있도록 .Domain(implements: .Entity) 의존성 추가
…ts 캐시

- OAuthLoginRequest 에 idToken: String? 추가 (CodingKey: identityToken) · redirectUri 옵셔널화
- AuthInterface / Default · Mock · AuthRepositoryImpl / AuthUseCaseImpl 의 login 시그니처에 idToken · redirectUri? 추가
- UnifiedOAuthUseCase.appleLogin: redirectUri = nil, idToken = payload.idToken 으로 호출하고 authCode · idToken 을 UserDefaults 에 동시 저장
- AuthLocalStorage 신규 — picke.auth.authCode / picke.auth.idToken 키로 단순 캐시 (Keychain 과 분리)
- Google · Kakao 는 기존 redirectUri 유지하고 idToken = nil 명시
- SplashFeature 의 keychain 자격 분기를 임시 주석 처리 (이후 Apple 로그인 흐름 검증 끝나면 복원 예정)
- AppReducer 가 splash.onAppear 시 곧바로 .view(.presentAuth) 를 dispatch 하도록 임시 변경
- pull_request (develop · main · master, opened/synchronize) 트리거
- Swift diff 만 추출하여 [LINE N] 어노테이션 후 모델에 전달 → 라인 단위 인라인 코멘트 게시
- 기존 Codex PR Review 워크플로우와 독립 실행되어 한쪽 실패가 다른 쪽에 영향 X
- secrets: GEMINI_API_KEY 필요 (repo Settings → Secrets → Actions)
…..) 자동 Bearer 토큰 첨부

- App 모듈에 .SPM.kingfisher 의존성 추가
- KingfisherConfigurator: AnyModifier 로 매 요청마다 KeychainManaging.accessToken() 을 Authorization 헤더에 자동 첨부
- AppDelegate 의 DI bootstrap 직후 호출하여 모든 KFImage 가 인증 토큰을 갖고 동작
- VoteCardView: 선택 전 단순 옵션 4개 → 선택 후 빈칸 채움 + secondary100/500 percentage bar 4 row (.pen `Radar Wrap`), 0 → target width easeOut 0.6s 애니메이션
- QuizCardView: .pen 단일 상태로 복원 (Result variant 없음)
- HomeSectionHeader: title 안의 라틴 단어(Best · Pické)와 한글 `배틀` 을 primary500 으로 자동 강조 (AttributedString)
- HeroCarouselView: thumbnail 영역 정밀 매핑 (375 wide + GeometryReader + clipped), var → @ViewBuilder func 변환
- NewBattleCardView: thumbnail 95h 복원 + 아바타 베이지 원형 배경 + 16×28 일러스트 ( .pen `Avatar/Philosopher` Frame 195 매칭)
- HotBattle mock 에 picsum thumbnail URL 추가하여 trendingBattles 빈 응답 시에도 이미지 표시
- HomeView · HomeSkeletonView 의 inner sub-view 들을 `@ViewBuilder` 명시
- 자식 ≥ 2개를 감싸거나 if/switch/ForEach 분기·반복이 있는 inner `private var`/`func` 에 `@ViewBuilder` 명시 (OnBoardingView · MainTabView · PreVoteView)
- AGENTS.md `#### 🧱 @ViewBuilder 함수 vs var` 규칙 섹션 추가 — 자식 개수/분기 유무로 함수+@ViewBuilder vs var 결정 기준 명문화
- Apple 로그인 동작 확인을 위해 임시로 비활성화했던 keychain 자격 분기를 복원
- SplashFeature 가 hasStoredCredential 에 따라 .presentMainTab / .presentAuth 로 분기
- AppReducer 의 splash.onAppear 핸들러도 .none 으로 원복하여 SplashFeature 의 delegate 흐름이 정상 작동
- Mode 1.tokens.json 폐기, primitive/semantic/component/$metadata JSON 도입
- Tools/TokenGenerator.swift 를 새 포맷에 맞게 재작성: 3-파일 deep-merge, 참조 체인 재귀 해석, rgba() 색 지원
- 자동 생성물 (ShapeStyle+.swift / CGFloat+Spacing+ / CGFloat+Radius+ / CGFloat+Component+ / ComponentToken.swift) 갱신
- Poll 도메인 추가 — Entity (PollDetail · PollOption · PollStatus + percentage helper), DTO+Mapper (yyyy-MM-dd · displayOrder 정렬), PollInterface · DefaultPollRepositoryImpl, PollRepositoryImpl (MoyaProvider<PollService>.authorized 패턴)
- API 도메인 PieckeDomain 에 case poll 추가 + AppDIManager 에 PollRepositoryImpl 등록
- PreVoteFeature: State 에 poll · isLoading · pollId 추가, Result + mapError 단일 Response 패턴으로 fetchPoll 비동기 액션 구성
- PreVoteView: onAppear 시 fetchPoll, 로딩 중에는 PreVoteSkeletonView 분기 표시
- PreVoteSkeletonView 신규 — .pen `사전 투표창 - Skeleton Loader` (nTffe) 의 10개 Rectangle 절대 좌표 1:1 매핑, 375pt 디자인 기준 GeometryReader 로 비례 scale
- DesignSystem 에 SkeletonView 신규 — beige600 base + 좌→우 shimmer 그라데이션 1.2s 무한 반복 (cornerRadius 옵션)
- HeroCarouselView · HotBattleCardView · NewBattleCardView 의 KFImage 에 .placeholder { SkeletonView() } 적용해 이미지 로딩 중 shimmer 표시
- Kingfisher 의 기본 캐싱(memory + disk) 정책은 그대로 사용
- Tokens Studio 단일 파일 (`Mode 1.tokens.json`) 적용에 맞춰 다중 분할 (component/primitive/semantic/$metadata) JSON 4개 정리
- ShapeStyle+ / ComponentToken / CGFloat 토큰 코드젠 결과 일괄 갱신
- LoginView: 정의되지 않은 `.gray200` / `.borderGray` → 동일 색의 alias `.neutral200` / `.borderGrayDefault` 로 교체
@Roy-wonji Roy-wonji added ⚙️ 환경설정 프로젝트 설정 ✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업 labels May 19, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kingfisher 설정, Poll 기능(API, DTO, Repository, Feature, View), Apple Sign-In idToken 전달, 홈 UI .pen 정밀 매칭 및 Skeleton, 디자인 토큰 코드젠 개편 등 주요 기능들이 잘 구현되었습니다. 특히 디자인 토큰 코드젠 업데이트는 아키텍처 개선에 크게 기여합니다. 몇 가지 모듈 의존성, 상속 및 코드 정리에 대한 개선점이 확인되었습니다.

import UIKit
import WeaveDI

import DomainInterface
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

모듈 임포트 순서를 UIKit이나 Foundation 계열 다음에 알파벳 순으로 정렬하는 것이 좋습니다. WeaveDIDomainInterface의 순서를 바꾸는 것을 고려해 보세요.

Suggested change
import DomainInterface
import WeaveDI
import DomainInterface

import Kingfisher

import DomainInterface
import Foundations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

Foundations 모듈이 DomainInterface보다 먼저 임포트되어야 할 특별한 이유가 없다면, 알파벳 순서로 정렬하는 것이 좋습니다.

Suggested change
import Foundations
import Foundations
import DomainInterface


@preconcurrency import AsyncMoya

public final class PollRepositoryImpl: PollInterface, @unchecked Sendable {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

@unchecked Sendable은 동시성 안전성을 명시적으로 보장할 수 없을 때 사용됩니다. PollRepositoryImpl의 현재 구현을 보면 MoyaProvider<PollService>는 스레드 안전하게 사용되므로, @unchecked를 제거하고 final class PollRepositoryImpl: PollInterface, Sendable로 선언하는 것이 더 명확하고 안전합니다. MoyaProvider는 내부적으로 Sendable을 따르고 있습니다.

guard let data = dto.data else {
let message = dto.error?.message ?? "투표 데이터 응답이 비어 있습니다"
Log.error("[PollRepositoryImpl] empty poll payload: \(message)")
throw AuthError.backendError(message)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3] Minor

PollRepositoryImpl에서 AuthError를 던지는 대신, PollError와 같은 Poll 도메인에 특화된 에러나 더 일반적인 NetworkError를 사용하는 것이 모듈의 책임과 Clean Architecture 원칙에 더 부합합니다. AuthErrorAuth 모듈에 한정되어야 합니다.

Suggested change
throw AuthError.backendError(message)
throw NetworkError.backendError(message)

settings: .settings(),
dependencies: [
.Data(implements: .API),
.Domain(implements: .Entity),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Data/Service 모듈은 Domain/Entity 모듈에 의존해서는 안 됩니다. Clean Architecture의 계층별 의존성 규칙(Presentation → Domain ← Data)에 따르면, Data 계층은 Domain 계층의 내용을 알지 못해야 합니다. EntityDomain 계층의 일부이며, Data/Service에서 이를 직접 임포트하는 것은 의존성 방향을 위반합니다. Data/Model에서 Entity로 매핑하고, Data/RepositoryEntity를 반환하는 것은 허용됩니다. PollService 자체는 Entity를 임포트하지 않으므로, 이 의존성은 잘못 등록된 것으로 보입니다.

Suggested change
.Domain(implements: .Entity),
// .Domain(implements: .Entity), // Data/Service는 Domain/Entity에 직접 의존하지 않아야 합니다.

public struct PreVoteView: View {
@Bindable public var store: StoreOf<PreVoteFeature>

public init(store: StoreOf<PreVoteFeature>) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

SwiftUI Viewinit은 대개 public으로 선언하는 것이 일반적이지만, PreVoteViewHomeCoordinator에서 직접 생성되므로 internal 접근 제어자만으로도 충분합니다. 이는 API 노출을 최소화하고 캡슐화를 강화하는 데 도움이 됩니다.

Suggested change
public init(store: StoreOf<PreVoteFeature>) {
internal init(store: StoreOf<PreVoteFeature>) {


@ViewBuilder
private func avatarView(_ philosopher: PhilosopherAvatar) -> some View {
let asset: ImageAsset = switch philosopher {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3] Minor

PhilosopherAvatar enumimageAsset computed property를 추가하여 이미지 애셋을 반환하도록 만들면, 이 avatarView 함수에서 switch 문을 중복으로 작성할 필요가 없어 코드를 더 간결하게 만들고 재사용성을 높일 수 있습니다. 이는 NewBattleCardViewprivate extension PhilosopherAvatar와 유사한 방식으로 구현할 수 있습니다.

Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift 파일의 PhilosopherAvatar enum에 추가하면 됩니다.

Suggested change
let asset: ImageAsset = switch philosopher {
public extension PhilosopherAvatar {
var imageAsset: ImageAsset {
switch self {
case .plato: .avatarPlato
case .sartre: .avatarSartre
case .sunja: .avatarSunja
}
}
}

그리고 avatarView에서는 다음과 같이 사용할 수 있습니다.

Suggested change
let asset: ImageAsset = switch philosopher {
return Image(asset: philosopher.imageAsset)

static var bedgeOutlineBackround: Color { .beige50 }
static var bedgeOutlineBorder: Color { .primary100 }
static var bedgeOutlineText: Color { .primary500 }
static var avatarBackround: Color { .beige600 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

avatarBackround에 오타가 있습니다. background가 올바른 철자입니다.

Suggested change
static var avatarBackround: Color { .beige600 }
static var avatarBackground: Color { .beige600 }

static var listItemAgreementBackgroundDefault: Color { .bgDefault }
static var listItemAgreementBorderDefault: Color { .gray50 }
static var listItemAgreementBorderError: Color { .init(hex: "C92D33", alpha: 0.4) }
static var listItemAgreementTextDefualt: Color { .gray800 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

listItemAgreementTextDefualt에 오타가 있습니다. Default가 올바른 철자입니다.

Suggested change
static var listItemAgreementTextDefualt: Color { .gray800 }
static var listItemAgreementTextDefault: Color { .gray800 }

public static var inverse: Color { .bedgeFilledBackgroundInverse }
}
public enum Avatar {
public static var backround: Color { .avatarBackround }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

backround에 오타가 있습니다. background가 올바른 철자입니다.

Suggested change
public static var backround: Color { .avatarBackround }
public static var background: Color { .avatarBackground }

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 016a1cb29d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +63 to +64
case .routeAction(_, action: .home(.delegate(.presentPreVote))):
state.routes.push(.preVote(.init()))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the selected battle id into pre-vote

When any home card sends presentPreVote(battleId:), this route handler discards the associated id and pushes PreVoteFeature.State() with its default pollId = 1. As a result, tapping hero/hot/best/new cards for any battle other than 1 still fetches and shares /poll/1, so users see the wrong poll despite HomeFeature forwarding the tapped battleId.

Useful? React with 👍 / 👎.

Comment on lines 115 to +117
VoteCardView(question: vote)
.contentShape(Rectangle())
.onTapGesture { }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire the vote card tap to its action

The Today Pické vote card now installs a tap gesture but leaves the handler empty, so tapping the card never sends voteTapped(vote) even though HomeFeature has the delegate path to open pre-vote. This makes the vote card the only home entry point that cannot navigate to the pre-vote screen.

Useful? React with 👍 / 👎.

Comment on lines +23 to +24
if let token = keychainManager.accessToken(), !token.isEmpty {
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict Bearer headers to protected image URLs

This global Kingfisher modifier attaches the access token to every KFImage request, not just Picke protected /api/v1/resources/... URLs. If any home image URL is external or user/API-controlled, the app will send the user's Bearer token to that host; the modifier should check the expected host/path before setting Authorization.

Useful? React with 👍 / 👎.

- protectedHostSuffixes 화이트리스트 (picke.store / dev.picke.store) 도입
- host suffix 정확 일치 + path.hasPrefix("/api/") + 토큰 존재 4-조건 만족 시에만 Authorization 헤더 첨부
- picsum.photos / 카카오 CDN 같은 외부 호스트로 keychain accessToken 이 새지 않도록 차단
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 매우 우수한 PR입니다. 새로운 사전 투표 기능의 TCA 아키텍처, Kingfisher의 보안 토큰 처리, 그리고 디자인 시스템 토큰의 대규모 업데이트가 모두 잘 구현되었습니다. 모듈 아키텍처, TCA 컨벤션, SwiftUI 컨벤션 및 Swift 코드 품질 가이드라인을 대부분 충족합니다. 몇 가지 사소한 개선 사항이 있습니다.

@@ -1,7 +1,7 @@
import ProjectDescription
import DependencyPackagePlugin
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

import DependencyPackagePlugin 선언은 프로젝트 상단에 이미 존재하며 중복됩니다. 제거해 주세요.

Suggested change
import DependencyPackagePlugin

import ProjectDescription
import DependencyPackagePlugin
import DependencyPlugin
import ProjectDescription
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

import ProjectDescription 선언은 프로젝트 상단에 이미 존재하며 중복됩니다. 제거해 주세요.

Suggested change
import ProjectDescription

let position: Int
let total: Int

private let thumbnailHeight: CGFloat = 220
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

HeroCarouselViewSelf.thumbnailHeight와 동일한 상수를 HeroCardView 내부에서 다시 선언하고 있습니다. 중복을 피하고 HeroCarouselViewSelf.thumbnailHeight를 사용하는 것이 좋습니다. 예를 들어 HeroCardView의 이니셜라이저를 통해 전달받거나, HeroCarouselView의 static 상수를 참조하도록 수정할 수 있습니다.

현재 HeroCardViewHeroCarouselView의 Subview이므로, HeroCardView에서 직접 HeroCarouselView.thumbnailHeight에 접근하기는 어렵습니다. 따라서 thumbnailHeightHeroCardView의 프로퍼티로 선언하고 HeroCarouselView에서 전달해 주는 방식이 더 적절해 보입니다.

if let vote = store.currentVote {
VoteCardView(question: vote)
.contentShape(Rectangle())
.onTapGesture { }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3] Minor

VoteCardView에 빈 onTapGesture가 있습니다. 현재는 아무 동작도 하지 않지만, HomeFeature.Action.voteTapped(VoteQuestion) 액션을 보내서 PreVoteView로 이동하는 등의 의도가 있었다면 이를 구현해야 합니다. 의도치 않게 추가된 경우라면 제거해 주세요.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1cc3ece6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

state.isLoading = false
switch result {
case let .success(poll):
state.poll = poll
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use fetched poll data for pre-vote content

When fetchPoll succeeds, the reducer only assigns state.poll, but the loaded pre-vote screen renders its title, summary, tags, background, and option cards from store.battle, which is initialized to PreVoteBattle.mock and never updated from the response. For any real poll whose content differs from the mock, users will see/select the hard-coded mock battle while the feature shares/submits the fetched poll id, so the poll details shown in production are wrong.

Useful? React with 👍 / 👎.

- PhilosopherAvatar.imageAsset 매핑을 Home 모듈 내부 internal extension (PhilosopherAvatar+ImageAsset.swift) 으로 추출
  - Entity 가 DesignSystem 에 의존하지 않도록 매핑은 Presentation 계층에 위치
- NewBattleCardView 의 private extension PhilosopherAvatar 제거 → 공용 extension 사용
- PreVoteView.avatarView 의 switch 문 제거 → Image(asset: philosopher.imageAsset) 한 줄
- HeroCardView 의 thumbnailHeight 중복 상수 제거 → init 매개변수로 받고 HeroCarouselView 가 Self.thumbnailHeight 주입 (단일 진실 공급원)
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 잘 구성된 PR입니다. 특히 Kingfisher의 전역 requestModifier 설정과 새로운 PreVoteFeature 및 관련 UI 컴포넌트들이 TCA 컨벤션과 SwiftUI 모범 사례를 잘 따르고 있습니다. Design System의 토큰 코드젠 결과도 잘 정리되어 컴포넌트 간 일관성이 향상되었습니다. 몇 가지 사소한 개선점과 잠재적인 문제를 발견했습니다.

import Kingfisher

import DomainInterface
import Foundations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

Foundations 모듈이 이 파일에서 실제로 사용되는지 확인해주세요. URLSetFoundation에서 제공되며, KeychainManagingDomainInterface에서 가져옵니다. 불필요한 import는 제거하는 것이 좋습니다.

Suggested change
import Foundations
// import Foundations

guard let data = dto.data else {
let message = dto.error?.message ?? "투표 데이터 응답이 비어 있습니다"
Log.error("[PollRepositoryImpl] empty poll payload: \(message)")
throw AuthError.backendError(message)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

PollRepositoryImpl에서 AuthError.backendError를 던지는 것은 의미상 적절하지 않습니다. 인증 관련 오류가 아닌 투표 데이터 관련 오류이므로, PollError와 같이 도메인 특화된 오류를 정의하거나 더 일반적인 NetworkError 또는 AppError를 사용하는 것이 좋습니다.

Suggested change
throw AuthError.backendError(message)
throw PollError.dataMissing(message) // PollError를 정의하거나 AppError 사용

import Foundation

import API
import Foundations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

Foundations 모듈이 이 파일에서 실제로 사용되는지 확인해주세요. MoyaAPI는 이미 필요한 기능을 제공합니다. 불필요한 import는 제거하는 것이 좋습니다.

Suggested change
import Foundations
// import Foundations

/// 강조 규칙:
/// - 라틴 단어 (Best · Pické 등) 가 있으면 그 단어만 primary500, 나머지 한글은 neutral900
/// - 라틴 단어가 없으면 한글 `배틀` 만 primary500, 나머지 한글은 neutral900
private var attributedTitle: AttributedString {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

View의 computed property에서 정규식을 사용한 복잡한 AttributedString 처리 로직이 구현되어 있습니다. 정적 텍스트의 경우 큰 문제는 없지만, 뷰 업데이트가 잦아지거나 텍스트가 동적으로 변경될 경우 성능 저하의 가능성이 있습니다. 이러한 텍스트 처리 로직은 Feature 또는 ViewModel 계층에서 미리 처리하여 AttributedString을 직접 전달하는 것을 고려해볼 수 있습니다.

if let vote = store.currentVote {
VoteCardView(question: vote)
.contentShape(Rectangle())
.onTapGesture { }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3] Minor

VoteCardView에 빈 onTapGesture 클로저가 있습니다. PR 설명의 테스트 플랜에 따르면, Vote 카드 탭 시 사전 투표창으로 진입해야 하므로, 여기에 send(.voteTapped(vote)) 액션을 호출하여 PreVoteView를 표시하는 로직을 추가해야 합니다.

Suggested change
.onTapGesture { }
.onTapGesture { send(.voteTapped(vote)) }

static var gray700: Color { .init(hex: "222120") }
static var gray800: Color { .init(hex: "1A1918") }
static var gray900: Color { .init(hex: "131212") }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P1] Critical

ShapeStyle+.swift 파일에 SPM.mixpanelSessionReplay와 같은 SPM 의존성 선언이 포함되어 있습니다. 이는 Project.swift에서 실수로 복사된 것으로 보이며, 컴파일 오류를 유발할 수 있는 치명적인 오류입니다. 즉시 제거해야 합니다.

Suggested change
// .SPM.mixpanelSessionReplay, // 이 줄을 제거해야 합니다.


@State private var phase: CGFloat = -1

private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

SkeletonView 내의 baseColor가 하드코딩된 RGB 값으로 정의되어 있습니다. Design System에 이미 beige600 토큰이 정의되어 있으므로, 이를 직접 사용하는 것이 일관성과 유지보수 측면에서 더 좋습니다.

Suggested change
private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0
private let baseColor = Color.beige600

@State private var phase: CGFloat = -1

private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0
private let shimmerColor = Color(red: 254 / 255, green: 254 / 255, blue: 253 / 255) // beige50 #FEFEFD
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

SkeletonView 내의 shimmerColor가 하드코딩된 RGB 값으로 정의되어 있습니다. Design System에 이미 beige50 토큰이 정의되어 있으므로, 이를 직접 사용하는 것이 일관성과 유지보수 측면에서 더 좋습니다.

Suggested change
private let shimmerColor = Color(red: 254 / 255, green: 254 / 255, blue: 253 / 255) // beige50 #FEFEFD
private let shimmerColor = Color.beige50

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f53ed2acd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +68 to +69
.routeAction(_, action: .preVote(.delegate(.submit))):
return .send(.view(.backAction))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Submit the pre-vote before popping the screen

When a user selects an option and taps primaryButtonTapped, PreVoteFeature sends .delegate(.submit(pollId:side:)), but this route handler treats that submit exactly like dismiss and immediately goes back. In this flow there is no repository call or parent action that records the selected side, so the CTA appears to complete a pre-vote while the vote is never persisted.

Useful? React with 👍 / 👎.

Comment on lines +41 to +42
git fetch origin "${{ github.event.pull_request.head.ref }}"
git diff "origin/${{ github.event.pull_request.base.ref }}"..."origin/${{ github.event.pull_request.head.ref }}" -- "*.swift" > diff.txt
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fetch fork PR heads from the PR ref

For pull requests opened from forks, origin points at the base repository, so fetching and diffing origin/${{ github.event.pull_request.head.ref }} only works when a branch with the same name exists in the base repo. In fork PRs this step fails before Gemini runs (or can diff the wrong branch if the name collides); fetch the PR ref/head SHA instead of the head branch name from origin.

Useful? React with 👍 / 👎.

Roy-wonji added 2 commits May 19, 2026 23:08
- 기존: pollResponse success 시 state.poll 만 갱신, View 는 mock 으로 초기화된 state.battle 만 사용 → 실제 응답이 화면에 반영 안 됨 + share/submit 은 새 pollId 로 동작해 콘텐츠 불일치
- makeBattle(from:fallback:) 헬퍼로 PollDetail → PreVoteBattle 변환: battleId/titlePrefix/titleSuffix/options 매핑, summary/tags/backgroundImageURL 은 API 에 없으므로 fallback (mock) 유지
- 옵션은 displayOrder 정렬된 상태에서 앞 2개를 좌/우 카드로, 부족하면 fallback option 사용. philosopher 매핑은 인덱스 기반 (.plato/.sartre/.sunja)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bf1ade835

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

case .home:
return"api/v1/home"
case .poll:
return"api/v1/poll"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the documented plural poll route

The new poll DTO/entity comments describe the detail API as GET /api/v1/polls/{pollId}, but this domain is singular and PollAPI.detailPoll appends /<id>, so fetchPoll will request /api/v1/poll/{id}. In environments where the backend exposes the documented plural route, every pre-vote detail fetch returns 404 and the screen remains on fallback/mock data; please align the base path with the poll detail endpoint.

Useful? React with 👍 / 👎.

Comment on lines +89 to +90
AuthLocalStorage.authCode = authCode
AuthLocalStorage.idToken = payload.idToken
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Apple credentials out of UserDefaults

On every successful Apple sign-in this writes the authorization code and identity token to UserDefaults via AuthLocalStorage, while the only clear() I found is never called. These values are login credentials/PII and persist outside the keychain even though the real session tokens are saved securely just below, so a logged-in user's Apple token can remain in app preferences/backups; keep them in memory, store them in the keychain, or clear them immediately after the backend exchange.

Useful? React with 👍 / 👎.

@Roy-wonji Roy-wonji merged commit bf1d09f into develop May 19, 2026
4 checks passed
@Roy-wonji Roy-wonji deleted the feature/-PreVote branch May 19, 2026 14:17
Roy-wonji added a commit that referenced this pull request May 19, 2026
- PhilosopherAvatar.imageAsset 매핑을 Home 모듈 내부 internal extension (PhilosopherAvatar+ImageAsset.swift) 으로 추출
  - Entity 가 DesignSystem 에 의존하지 않도록 매핑은 Presentation 계층에 위치
- NewBattleCardView 의 private extension PhilosopherAvatar 제거 → 공용 extension 사용
- PreVoteView.avatarView 의 switch 문 제거 → Image(asset: philosopher.imageAsset) 한 줄
- HeroCardView 의 thumbnailHeight 중복 상수 제거 → init 매개변수로 받고 HeroCarouselView 가 Self.thumbnailHeight 주입 (단일 진실 공급원)
@github-actions
Copy link
Copy Markdown

{
"summary": "전반적으로 새 기능 구현 및 아키텍처 원칙을 잘 준수하고 있습니다. 특히 Kingfisher Request Modifier를 통한 이미지 인증 처리와 Apple Sign-In 연동, 새로운 Poll 기능 구현 및 TCA 로직 분리 등은 모범적입니다. 다만, SwiftUI 뷰 계층 구조 관련 컨벤션 (하위 뷰는 struct로 선언)과 일부 디자인 토큰 사용에서 개선이 필요합니다.",
"comments": [
{
"path": "Projects/App/Project.swift",
"line": 19,
"code_snippet": " .SPM.mixpanelSessionReplay,",
"body": "⚪ [P5] Nitpick\n\n줄 끝의 불필요한 쉼표입니다. 다음 줄에 항목을 추가하면서 발생한 것으로 보입니다."
},
{
"path": "Projects/App/Project.swift",
"line": 21,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/App/Sources/Application/AppDelegate.swift",
"line": 22,
"code_snippet": " "token_prefix": String((mixPanelKey ?? "").prefix(6)),",
"body": "⚪ [P5] Nitpick\n\n줄 끝의 불필요한 쉼표입니다."
},
{
"path": "Projects/App/Sources/Application/AppDelegate.swift",
"line": 44,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/App/Sources/Application/AppDelegate.swift",
"line": 57,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/App/Sources/Application/AppDelegate.swift",
"line": 65,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/App/Sources/Application/KingfisherConfigurator.swift",
"line": 16,
"code_snippet": "enum KingfisherConfigurator {",
"body": "🔵 [P4] Readability\n\nKingfisherConfigurator는 인스턴스 생성이 필요 없고 오직 정적(static) 메서드만 제공하는 유틸리티 타입이므로, enum 대신 final class로 선언하고 init()private으로 두는 것이 Swift API Design Guidelines에 더 적합합니다.\nsuggestion\nfinal class KingfisherConfigurator {\n private init() {}\n\n /// 보호 이미지 (picke 백엔드 `/api/v1/resources/...`) 에만 Bearer 토큰을 첨부한다.\n /// 그 외 외부 호스트 (picsum.photos / 카카오 CDN 등) 로는 토큰을 절대 보내지 않는다.\n private static let protectedHostSuffixes: Set<String> = [\n \"picke.store\",\n \"dev.picke.store\",\n ]\n\n static func configureAuthorizedDownloader(\n keychainManager: KeychainManaging\n ) {\n let modifier = AnyModifier { request in\n var req = request\n\n guard\n let url = req.url,\n let host = url.host?.lowercased(),\n protectedHostSuffixes.contains(where: { host == $0 || host.hasSuffix(\".\\($0)\") }),\n url.path.hasPrefix(\"/api/\"),\n let token = keychainManager.accessToken(), !token.isEmpty\n else {\n return req\n }\n\n req.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n return req\n }\n\n KingfisherManager.shared.defaultOptions = [\n .requestModifier(modifier),\n ]\n }\n}\n"
},
{
"path": "Projects/Data/API/Sources/Poll/PollAPI.swift",
"line": 10,
"code_snippet": "public enum PollAPI {",
"body": "⚪ [P5] Nitpick\n\nPollAPI 타입 이름 뒤에 불필요한 공백이 있습니다.\nsuggestion\npublic enum PollAPI {\n"
},
{
"path": "Projects/Data/API/Sources/Poll/PollAPI.swift",
"line": 13,
"code_snippet": " public var description: String {",
"body": "🔵 [P4] Readability\n\ndescription 프로퍼티는 CustomStringConvertible 프로토콜에서 주로 사용되므로, API 경로를 나타내는 프로퍼티 이름으로는 urlPath 또는 endpointPath와 같이 더 명확한 이름을 사용하는 것이 좋습니다. (BaseTargetTypeurlPath와 일관성을 유지할 수 있습니다.)"
},
{
"path": "Projects/Presentation/Auth/Sources/Main/View/LoginView.swift",
"line": 14,
"code_snippet": "public struct LoginView: View {",
"body": "⚪ [P5] Nitpick\n\nLoginView 선언부의 불필요한 공백 제거는 좋으나, LoginView 내의 logoView(), loginSNSButtonText(), logjnButton() 등의 보조 뷰들이 private func 또는 private var 형태로 구현되어 있습니다. SwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따라 이들은 재사용 가능한 별도의 private struct로 분리하는 것이 좋습니다."
},
{
"path": "Projects/Presentation/Auth/Sources/Main/View/LoginView.swift",
"line": 19,
"code_snippet": " Color.gray50",
"body": "🔵 [P4] Readability\n\n.gray50은 Primitive Color Token입니다. 배경색과 같이 UI의 특정 역할에 사용되는 경우에는 .bgSubtler (현재 .gray50을 참조하는 Semantic Token)와 같은 Semantic Color Token을 사용하는 것이 디자인 시스템의 의도와 일관성을 높입니다.\nsuggestion\n Color.bgSubtler\n"
},
{
"path": "Projects/Presentation/Auth/Sources/OnBoarding/View/OnBoardingView.swift",
"line": 42,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, topSection()과 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다.\n\n이는 앱의 복잡도가 증가할 때 뷰 계층 구조의 명확성을 저해하고, .body 프로퍼티의 부하를 증가시킬 수 있습니다.\n\n마찬가지로, pageContent(), titleBlock(), illustration(), bottomSection() 또한 private struct로 분리하는 것을 고려해 주세요."
},
{
"path": "Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift",
"line": 117,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift",
"line": 120,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift",
"line": 123,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift",
"line": 126,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift",
"line": 129,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift",
"line": 73,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, controlRow(), thumbnail(), subject()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift",
"line": 101,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, controlRow(), thumbnail(), subject()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift",
"line": 122,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift",
"line": 146,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, controlRow(), thumbnail(), subject()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift",
"line": 157,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 34,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 42,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 50,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 67,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 85,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 106,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 134,
"code_snippet": " private func avatar(",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift",
"line": 154,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, content, container, metaRow, titleBlock, versusRow, admissionButton, avatar, vsBadge와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift",
"line": 33,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), titleBlock(), options() 등의 보조 뷰들은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift",
"line": 44,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), titleBlock(), options() 등의 보조 뷰들은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift",
"line": 59,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), titleBlock(), options() 등의 보조 뷰들은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift",
"line": 67,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), titleBlock(), options() 등의 보조 뷰들은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 22,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 24,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 29,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 55,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 72,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 95,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 118,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 127,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 148,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 151,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 173,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, header(), heading(), answerSlot(), grid(), optionButton(), resultBars(), resultBarRow()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift",
"line": 194,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift",
"line": 137,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, todayQuizCard(), todayVoteCard(), shimmer와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift",
"line": 165,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, todayQuizCard(), todayVoteCard(), shimmer와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift",
"line": 309,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, todayQuizCard(), todayVoteCard(), shimmer와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeView.swift",
"line": 68,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, hotBattlesSection(), bestBattlesSection(), todayPickeSection(), newBattlesSection()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeView.swift",
"line": 87,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, hotBattlesSection(), bestBattlesSection(), todayPickeSection(), newBattlesSection()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeView.swift",
"line": 104,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, hotBattlesSection(), bestBattlesSection(), todayPickeSection(), newBattlesSection()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeView.swift",
"line": 117,
"code_snippet": " .onTapGesture { }",
"body": "🟡 [P3] Minor\n\nVoteCardViewonTapGesture { }가 비어있는 채로 추가되었습니다. 현재는 아무런 동작을 하지 않는 상태이므로, 의도치 않은 사용자 인터랙션이 발생하거나 기능이 미완성되었다는 오해를 줄 수 있습니다. 만약 탭 이벤트를 통해 PreVoteFeature로 이동해야 한다면 해당 로직을 구현해야 합니다.\nsuggestion\n .onTapGesture { send(.voteTapped(vote)) }\n"
},
{
"path": "Projects/Presentation/Home/Sources/Main/View/HomeView.swift",
"line": 124,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, hotBattlesSection(), bestBattlesSection(), todayPickeSection(), newBattlesSection()와 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 8,
"code_snippet": "import Foundation",
"body": "⚪ [P5] Nitpick\n\n빈 줄이 너무 많습니다. Swift 파일 상단에서 import 문 그룹은 한 줄의 공백으로 구분하는 것이 일반적입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 10,
"code_snippet": "import ComposableArchitecture",
"body": "⚪ [P5] Nitpick\n\n빈 줄이 너무 많습니다. Swift 파일 상단에서 import 문 그룹은 한 줄의 공백으로 구분하는 것이 일반적입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 14,
"code_snippet": "import LogMacro",
"body": "⚪ [P5] Nitpick\n\n빈 줄이 너무 많습니다. Swift 파일 상단에서 import 문 그룹은 한 줄의 공백으로 구분하는 것이 일반적입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 19,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 29,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 33,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 36,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 41,
"code_snippet": " public init(id: UUID = UUID(), items: [String]) {",
"body": "⚪ [P5] Nitpick\n\n함수 파라미터가 닫는 괄호와 같은 줄에 있습니다. 가독성을 위해 파라미터를 한 줄씩 분리하고 닫는 괄호는 별도의 줄에 위치하도록 합니다.\nsuggestion\n public init(\n id: UUID = UUID(),\n items: [String]\n ) {\n"
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 47,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 55,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 64,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 68,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 72,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 77,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 81,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 83,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 90,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 93,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 96,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 99,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 104,
"code_snippet": " }",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 106,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 116,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 119,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 130,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 134,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 141,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 160,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 178,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 195,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 206,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift",
"line": 217,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 7,
"code_snippet": "//",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 주석 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 8,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 10,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 12,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 16,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 23,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 26,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 29,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 34,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 37,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 40,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 44,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 47,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift",
"line": 61,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, block()과 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 7,
"code_snippet": "//",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 주석 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 8,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 10,
"code_snippet": "import ComposableArchitecture",
"body": "⚪ [P5] Nitpick\n\n빈 줄이 너무 많습니다. Swift 파일 상단에서 import 문 그룹은 한 줄의 공백으로 구분하는 것이 일반적입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 14,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 18,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 42,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 47,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, loadedContent, backgroundImage, navigationBar, contentArea, contentSection, tagsRow, titleText, summaryText, optionSection, optionCard(_:), avatarView(_:), vsBadge, primaryButton과 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 60,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 64,
"code_snippet": " @ViewBuilder",
"body": "🟡 [P3] Minor\n\nSwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions) 규칙에 따르면, loadedContent, backgroundImage, navigationBar, contentArea, contentSection, tagsRow, titleText, summaryText, optionSection, optionCard(_:), avatarView(_:), vsBadge, primaryButton과 같은 재사용 가능한 UI 블록은 private struct로 선언하는 것이 좋습니다. @ViewBuilder를 사용하는 private var 또는 private func 형태는 규칙에 명시된 'NOT @ViewBuilder functions'에 해당합니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 77,
"code_snippet": "",
"body": "⚪ [P5] Nitpick\n\n불필요한 빈 줄입니다."
},
{
"path": "Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift",
"line": 86,
"code_

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚙️ 환경설정 프로젝트 설정 ✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant